package service

import (
	"context"
	"fmt"
	"sort"
	"time"

	"database/sql"

	"gitee.com/hp-huiw/family-expense-api/internal/constant"
	"gitee.com/hp-huiw/family-expense-api/internal/dto"
	"gitee.com/hp-huiw/family-expense-api/internal/repository"
	"gitee.com/hp-huiw/my-go-core/date"
	"gitee.com/hp-huiw/my-go-core/util"
	"github.com/samber/lo"
	"github.com/samber/lo/mutable"
)

type IncomeExpenseAnalysisService interface {
	QueryMonthlySummaryList(ctx context.Context, accountGroupID string, dates []time.Time) ([]dto.AnalysisSummaryResponse, error)
	QueryDailySummaryList(ctx context.Context, accountGroupID string, dates []time.Time) ([]dto.AnalysisSummaryResponse, error)
	QueryExistingDistinctYears(ctx context.Context, accountGroupID string) ([]int, error)
	QueryAnnualMonthlySummary(ctx context.Context, accountGroupID string, incomeExpenseType string, paymentType string, year int) (map[string]any, error)
	QueryMonthlyDailySummary(ctx context.Context, accountGroupID string, incomeExpenseType string, paymentType string, year int, month int) (map[string]any, error)
	QueryTopRank(ctx context.Context, accountGroupID string, pageSize int64, pageIndex int64, incomeExpenseType string, paymentType string, year int, month int) ([]dto.IncomeExpenseDetailResponse, error)
	QueryExpense(ctx context.Context, accountGroupID string, paymentType string, year int, month int) ([]dto.DetailTypeSummaryResponse, error)
	QueryIncome(ctx context.Context, accountGroupID string, paymentType string, year int, month int) ([]dto.DetailTypeSummaryResponse, error)
	QueryOverallSummary(ctx context.Context, accountGroupID string) (map[string]map[string]any, error)
	QueryAnnualSummary(ctx context.Context, accountGroupID string, years []int) ([]dto.AnnualAnalysisSummaryResponse, error)
}

// NewIncomeExpenseAnalysisService returns a new instance of IncomeExpenseAnalysisService.
func NewIncomeExpenseAnalysisService(db *sql.DB) IncomeExpenseAnalysisService {
	return &incomeExpenseAnalysisService{
		db:                            db,
		incomeExpenseDetailRepository: repository.NewIncomeExpenseDetailRepository(db),
		userService:                   NewUserService(db),
	}
}

// QueryMonthlySummaryList queries income expense summary based one month. Includes total income, total expense and balance.
func (s *incomeExpenseAnalysisService) QueryMonthlySummaryList(ctx context.Context, accountGroupID string, dates []time.Time) ([]dto.AnalysisSummaryResponse, error) {
	result := []dto.AnalysisSummaryResponse{}

	if len(dates) > 0 {
		uniqueDates := map[time.Time]time.Time{}

		// Gets unique date from the list.
		for _, udate := range dates {
			firstDay := date.GetFirstDayOfMonth(udate)
			uniqueDates[firstDay] = firstDay
		}

		for _, date := range uniqueDates {
			rawData, err := s.incomeExpenseDetailRepository.QueryMonthlySummaryList(ctx, accountGroupID, date)

			if err != nil {
				return nil, err
			}

			cashIncome := s.calculateTotalAmount(rawData, constant.PaymentTypeCash, constant.IncomeExpenseTypeIncome)
			cashExpense := s.calculateTotalAmount(rawData, constant.PaymentTypeCash, constant.IncomeExpenseTypeExpense)
			creditCardIncome := s.calculateTotalAmount(rawData, constant.PaymentTypeCreditCard, constant.IncomeExpenseTypeIncome)
			creditCardExpense := s.calculateTotalAmount(rawData, constant.PaymentTypeCreditCard, constant.IncomeExpenseTypeExpense)

			result = append(result, dto.AnalysisSummaryResponse{
				IncomeExpenseDate: date,
				AnalysisResultResponse: dto.AnalysisResultResponse{
					CashIncome:        cashIncome,
					CashExpense:       cashExpense,
					CashBalance:       cashIncome + cashExpense,
					CreditCardIncome:  creditCardIncome,
					CreditCardExpense: creditCardExpense,
					CreditCardBalance: creditCardIncome + creditCardExpense,
				},
			})
		}
	}

	return result, nil
}

// QueryDailySummaryList queries income expense summary based one single day. Includes total income, total expense and balance.
func (s *incomeExpenseAnalysisService) QueryDailySummaryList(ctx context.Context, accountGroupID string, dates []time.Time) ([]dto.AnalysisSummaryResponse, error) {
	dateMap := map[time.Time]time.Time{}

	// Gets unique date from the list.
	for _, date := range dates {
		dateMap[date] = date
	}

	result := []dto.AnalysisSummaryResponse{}

	if len(dateMap) > 0 {
		uniqueDates := append([]time.Time(nil), dates...)
		query, err := s.incomeExpenseDetailRepository.QueryDailySummaryList(ctx, accountGroupID, uniqueDates)

		if err != nil {
			return nil, err
		}

		for _, item := range query {
			tempItems := lo.Filter(query, func(innerItem repository.DatedAmountSummary, _ int) bool {
				return innerItem.IncomeExpenseDate.Equal(item.IncomeExpenseDate)
			})

			dateItems := append([]repository.DatedAmountSummary(nil), tempItems...)

			cashIncome := s.calculateTotalAmountWithDatedSummary(dateItems, constant.PaymentTypeCash, constant.IncomeExpenseTypeIncome)
			cashExpense := s.calculateTotalAmountWithDatedSummary(dateItems, constant.PaymentTypeCash, constant.IncomeExpenseTypeExpense)
			creditCardIncome := s.calculateTotalAmountWithDatedSummary(dateItems, constant.PaymentTypeCreditCard, constant.IncomeExpenseTypeIncome)
			creditCardExpense := s.calculateTotalAmountWithDatedSummary(dateItems, constant.PaymentTypeCreditCard, constant.IncomeExpenseTypeExpense)

			result = append(result, dto.AnalysisSummaryResponse{
				IncomeExpenseDate: item.IncomeExpenseDate,
				AnalysisResultResponse: dto.AnalysisResultResponse{
					CashIncome:        cashIncome,
					CashExpense:       cashExpense,
					CashBalance:       cashIncome + cashExpense,
					CreditCardIncome:  creditCardIncome,
					CreditCardExpense: creditCardExpense,
					CreditCardBalance: creditCardIncome + creditCardExpense,
				},
			})
		}
	}

	return result, nil
}

// QueryExistingDistinctYears queries all distinct income/expense years.
// return The list of Integer of existing year.
func (s *incomeExpenseAnalysisService) QueryExistingDistinctYears(ctx context.Context, accountGroupID string) ([]int, error) {
	years, err := s.incomeExpenseDetailRepository.QueryExistingDistinctYears(ctx, accountGroupID)

	if err != nil {
		return nil, err
	}

	sort.Ints(years)
	mutable.Reverse(years)

	return years, nil
}

// QueryAnnualMonthlySummary queries the summary based on the type and year for each month.
// return The map instance which contains keys below.
//
//	(year): The double of the total amount of this year.
//	(month): The double of the total amount of this month.
func (s *incomeExpenseAnalysisService) QueryAnnualMonthlySummary(ctx context.Context, accountGroupID string, incomeExpenseType string, paymentType string, year int) (map[string]any, error) {
	result := map[string]any{}
	totalAmount := 0.0
	existingMonthlyAmountList, err := s.incomeExpenseDetailRepository.QueryPeriodSummary(ctx, accountGroupID, incomeExpenseType, paymentType, year, 0)

	if err != nil {
		return nil, err
	}

	// Prepares the amount of 12 months based on existing monthly amount data.
	for i := range 12 {
		// i is 0 based.
		month := i + 1

		// The amount of each month.
		amount := 0.0

		// Gets the monthly amount data from query result for current month.
		monthlyAmount := lo.Filter(existingMonthlyAmountList, func(item repository.PeriodAmount, _ int) bool {
			return item.Period == month
		})

		// If monthly data exists then updates amount.
		if len(monthlyAmount) > 0 {
			amount += monthlyAmount[0].Amount
		}

		// Calculates the annual total amount.
		totalAmount += amount

		// Formats the month to string. Example: 1 to 01.
		// %02d means pad with zeroes (width 2).
		// https://yourbasic.org/golang/fmt-printf-reference-cheat-sheet/
		monthString := fmt.Sprintf("%02d", month)

		result[monthString] = amount
	}

	result[util.IntToString(year)] = totalAmount

	return result, nil
}

// Queries the summary based on the type and month for each day.
// return The map instance which contains keys below.
//
//	(year): The double of the total amount of this year.
//	(month): The double of the total amount of this month.
func (s *incomeExpenseAnalysisService) QueryMonthlyDailySummary(ctx context.Context, accountGroupID string, incomeExpenseType string, paymentType string, year int, month int) (map[string]any, error) {
	result := map[string]any{}
	existingDailyAmountList, err := s.incomeExpenseDetailRepository.QueryPeriodSummary(ctx, accountGroupID, incomeExpenseType, paymentType, year, month)

	if err != nil {
		return nil, err
	}

	totalAmount := 0.0
	firstDay, _ := date.ParseDateInDefaultLocation(year, month, 1)
	lastDay := date.GetLastDayOfMonth(firstDay)

	// Prepares the amount of days based on existing daily amount data.
	for i := 0; i < lastDay.Day(); i++ {
		// i is 0 based.
		day := i + 1

		// The amount of each day.
		amount := 0.0

		// Gets the daily amount data from query result for current day.
		dailyAmount := lo.Filter(existingDailyAmountList, func(item repository.PeriodAmount, _ int) bool {
			return item.Period == day
		})

		// If daily data exists then updates amount.
		if len(dailyAmount) > 0 {
			amount += dailyAmount[0].Amount
		}

		// Calculates the annual total amount.
		totalAmount += amount

		// Formats the month to string. Example: 1 to 01.
		// %02d means pad with zeroes (width 2).
		// https://yourbasic.org/golang/fmt-printf-reference-cheat-sheet/
		dayString := fmt.Sprintf("%02d", day)

		result[dayString] = amount
	}

	result[util.IntToString(year)+util.IntToString(month)] = totalAmount

	return result, nil
}

// QueryTopRank queries the top income/expense order by amount based on type and payment type in particular year and month.
func (s *incomeExpenseAnalysisService) QueryTopRank(ctx context.Context, accountGroupID string, pageSize int64, pageIndex int64, incomeExpenseType string, paymentType string, year int, month int) ([]dto.IncomeExpenseDetailResponse, error) {
	query, err := s.incomeExpenseDetailRepository.QueryTopRank(ctx, accountGroupID, pageSize, pageIndex, incomeExpenseType, paymentType, year, month)

	if err != nil {
		return nil, err
	}

	result := lo.Map(query, func(item repository.IncomeExpenseDetail, _ int) dto.IncomeExpenseDetailResponse {
		return *ToIncomeExpenseDetailDTO(&item)
	})

	return result, nil
}

// QueryExpense queries the summary based on the year and month, type and detail types.
// return The list of map instance which contains keys below.
//
//	detailType: The sting of the detail type. The value is corresponding amount.
//	amount: The corresponding amount of this detail type.
func (s *incomeExpenseAnalysisService) QueryExpense(ctx context.Context, accountGroupID string, paymentType string, year int, month int) ([]dto.DetailTypeSummaryResponse, error) {
	detailTypes, err := NewIncomeExpenseDetailTypeService(s.db).QueryNameForAnalysis(ctx, accountGroupID, constant.IncomeExpenseTypeExpense)

	if err != nil {
		return nil, err
	}

	query, err := s.incomeExpenseDetailRepository.QuerySummaryByDetailType(ctx, accountGroupID, paymentType, year, month, constant.IncomeExpenseTypeExpense, detailTypes)

	if err != nil {
		return nil, err
	}

	return s.toDetailTypeSummaryDTOs(query), nil
}

// QueryIncome queries the income summary based on the type and year.
// return The map instance which contains keys below.
//
//	detailType: The sting of the detail type. The value is corresponding amount.
//	amount: The corresponding amount of this detail type.
func (s *incomeExpenseAnalysisService) QueryIncome(ctx context.Context, accountGroupID string, paymentType string, year int, month int) ([]dto.DetailTypeSummaryResponse, error) {
	detailTypes, err := NewIncomeExpenseDetailTypeService(s.db).QueryNameForAnalysis(ctx, accountGroupID, constant.IncomeExpenseTypeIncome)

	if err != nil {
		return nil, err
	}

	query, err := s.incomeExpenseDetailRepository.QuerySummaryByDetailType(ctx, accountGroupID, paymentType, year, month, constant.IncomeExpenseTypeIncome, detailTypes)

	if err != nil {
		return nil, err
	}

	return s.toDetailTypeSummaryDTOs(query), nil
}

func (s *incomeExpenseAnalysisService) QueryOverallSummary(ctx context.Context, accountGroupID string) (map[string]map[string]any, error) {
	result := map[string]map[string]any{
		"cashIncomeSummary":        {},
		"cashExpenseSummary":       {},
		"creditCardIncomeSummary":  {},
		"creditCardExpenseSummary": {},
	}

	overallSummary, err := s.incomeExpenseDetailRepository.QueryOverallSummary(ctx, accountGroupID)

	if err != nil {
		return nil, err
	}

	distinctYears := lo.Uniq(
		lo.Map(overallSummary, func(item repository.OverallSummary, _ int) int {
			return item.Year
		}),
	)

	cashIncomeSummary := 0.0
	totalCashIncomeSummary := 0.0
	cashExpenseSummary := 0.0
	totalCashExpenseSummary := 0.0
	creditCardIncomeSummary := 0.0
	totalCreditCardIncomeSummary := 0.0
	creditCardExpenseSummary := 0.0
	totalCreditCardExpenseSummary := 0.0
	fallbackSummary := repository.OverallSummary{Amount: 0.0}

	for _, year := range distinctYears {
		// Calculating cashIncomeSummary.
		cashIncomeSummary = lo.FindOrElse(overallSummary, fallbackSummary, func(summary repository.OverallSummary) bool {
			return summary.Year == year &&
				summary.Type == constant.IncomeExpenseTypeIncome &&
				summary.PaymentType == constant.PaymentTypeCash
		}).Amount

		totalCashIncomeSummary += cashIncomeSummary
		result["cashIncomeSummary"][util.IntToString(year)] = cashIncomeSummary

		// Calculating cashExpenseSummary.
		cashExpenseSummary = lo.FindOrElse(overallSummary, fallbackSummary, func(summary repository.OverallSummary) bool {
			return summary.Year == year &&
				summary.Type == constant.IncomeExpenseTypeExpense &&
				summary.PaymentType == constant.PaymentTypeCash
		}).Amount

		totalCashExpenseSummary += cashExpenseSummary

		result["cashExpenseSummary"][util.IntToString(year)] = cashExpenseSummary

		// Calculating creditCardIncomeSummary.
		creditCardIncomeSummary = lo.FindOrElse(overallSummary, fallbackSummary, func(summary repository.OverallSummary) bool {
			return summary.Year == year &&
				summary.Type == constant.IncomeExpenseTypeIncome &&
				summary.PaymentType == constant.PaymentTypeCreditCard
		}).Amount

		totalCreditCardIncomeSummary += creditCardIncomeSummary
		result["creditCardIncomeSummary"][util.IntToString(year)] = creditCardIncomeSummary

		// Calculating creditCardExpenseSummary.
		creditCardExpenseSummary = lo.FindOrElse(overallSummary, fallbackSummary, func(summary repository.OverallSummary) bool {
			return summary.Year == year &&
				summary.Type == constant.IncomeExpenseTypeExpense &&
				summary.PaymentType == constant.PaymentTypeCreditCard
		}).Amount

		totalCreditCardExpenseSummary += creditCardExpenseSummary
		result["creditCardExpenseSummary"][util.IntToString(year)] = creditCardExpenseSummary
	}

	result["cashIncomeSummary"]["total"] = totalCashIncomeSummary
	result["cashExpenseSummary"]["total"] = totalCashExpenseSummary
	result["creditCardIncomeSummary"]["total"] = totalCreditCardIncomeSummary
	result["creditCardExpenseSummary"]["total"] = totalCreditCardExpenseSummary

	return result, nil
}

// Queries the annual summary in the specific years.
// return The list of map instance which contains keys below.
//
//	year: The year of these summary.
//	cashIncome: The total cash income amount of this year.
//	cashExpense: The total cash expense amount of this year.
//	cashBalance: The total cash balance amount of this year.
//	creditCardIncome: The total credit card income amount of this year.
//	creditCardExpense: The total credit card expense amount of this year.
//	creditCardBalance: The total credit card balance amount of this year.
func (s *incomeExpenseAnalysisService) QueryAnnualSummary(ctx context.Context, accountGroupID string, years []int) ([]dto.AnnualAnalysisSummaryResponse, error) {
	result := []dto.AnnualAnalysisSummaryResponse{}

	if len(years) > 0 {
		summaryList, err := s.incomeExpenseDetailRepository.QueryAnnualSummary(ctx, accountGroupID, years)

		if err != nil {
			return nil, err
		}

		var totalCashIncome float64 = 0.0
		var totalCashExpense float64 = 0.0
		var totalCreditCardIncome float64 = 0.0
		var totalCreditCardExpense float64 = 0.0
		var cashIncome float64
		var cashExpense float64
		var creditCardIncome float64
		var creditCardExpense float64
		fallbackSummary := repository.OverallSummary{Amount: 0.0}

		for _, year := range years {
			cashIncome = lo.FindOrElse(summaryList, fallbackSummary, func(summary repository.OverallSummary) bool {
				return summary.Year == year &&
					summary.Type == constant.IncomeExpenseTypeIncome &&
					summary.PaymentType == constant.PaymentTypeCash
			}).Amount

			cashExpense = lo.FindOrElse(summaryList, fallbackSummary, func(summary repository.OverallSummary) bool {
				return summary.Year == year &&
					summary.Type == constant.IncomeExpenseTypeExpense &&
					summary.PaymentType == constant.PaymentTypeCash
			}).Amount

			creditCardIncome = lo.FindOrElse(summaryList, fallbackSummary, func(summary repository.OverallSummary) bool {
				return summary.Year == year &&
					summary.Type == constant.IncomeExpenseTypeIncome &&
					summary.PaymentType == constant.PaymentTypeCreditCard
			}).Amount

			creditCardExpense = lo.FindOrElse(summaryList, fallbackSummary, func(summary repository.OverallSummary) bool {
				return summary.Year == year &&
					summary.Type == constant.IncomeExpenseTypeExpense &&
					summary.PaymentType == constant.PaymentTypeCreditCard
			}).Amount

			totalCashIncome += cashIncome
			totalCashExpense += cashExpense
			totalCreditCardIncome += creditCardIncome
			totalCreditCardExpense += creditCardExpense

			result = append(result, dto.AnnualAnalysisSummaryResponse{
				Year: util.IntToString(year),
				AnalysisResultResponse: dto.AnalysisResultResponse{
					CashIncome:        cashIncome,
					CashExpense:       cashExpense,
					CashBalance:       cashIncome + cashExpense,
					CreditCardIncome:  creditCardIncome,
					CreditCardExpense: creditCardExpense,
					CreditCardBalance: creditCardIncome + creditCardExpense,
				},
			})
		}

		result = util.InsertSlice(result, dto.AnnualAnalysisSummaryResponse{
			Year: fmt.Sprintf("%v - %v", years[len(years)-1], years[0]),
			AnalysisResultResponse: dto.AnalysisResultResponse{
				CashIncome:        totalCashIncome,
				CashExpense:       totalCashExpense,
				CashBalance:       totalCashIncome + totalCashExpense,
				CreditCardIncome:  totalCreditCardIncome,
				CreditCardExpense: totalCreditCardExpense,
				CreditCardBalance: totalCreditCardIncome + totalCreditCardExpense,
			},
		}, 0)
	}

	return result, nil
}

func (*incomeExpenseAnalysisService) toDetailTypeSummaryDTOs(query []repository.DetailTypeSummary) []dto.DetailTypeSummaryResponse {
	return lo.Map(query, func(item repository.DetailTypeSummary, _ int) dto.DetailTypeSummaryResponse {
		return dto.DetailTypeSummaryResponse{
			DetailType: item.DetailType,
			Amount:     item.Amount,
		}
	})
}

// Calculates total amount from input slice with provided payment type and income/expense type.
func (*incomeExpenseAnalysisService) calculateTotalAmount(items []repository.AmountSummary, paymentType string, incomeExpenseType string) float64 {
	filteredItems := lo.Filter(items, func(item repository.AmountSummary, _ int) bool {
		return item.PaymentType == paymentType &&
			item.Type == incomeExpenseType
	})

	totalAmount := 0.00

	for i := range filteredItems {
		totalAmount += filteredItems[i].Amount
	}

	return totalAmount
}

// Calculates total amount from input slice with provided payment type and income/expense type.
func (s *incomeExpenseAnalysisService) calculateTotalAmountWithDatedSummary(items []repository.DatedAmountSummary, paymentType string, incomeExpenseType string) float64 {
	filteredItems := lo.Map(items, func(item repository.DatedAmountSummary, _ int) repository.AmountSummary {
		return repository.AmountSummary{
			Amount:      item.Amount,
			PaymentType: item.PaymentType,
			Type:        item.Type,
		}
	})

	return s.calculateTotalAmount(filteredItems, paymentType, incomeExpenseType)
}

// Container of IncomeExpenseAnalysisService functions.
type incomeExpenseAnalysisService struct {
	db                            *sql.DB
	incomeExpenseDetailRepository repository.IncomeExpenseDetailRepository
	userService                   UserService
}
